﻿#include <vector>//向量，模型的face就是用vector<int>存储的

#include "tgaimage.h"//用来读取生成tga图片的
#include "geometry.h"//定义了一些Vec2i,Vec3f的结构和函数
#include"model.h"//用来读取模型，也就是.obj,里面有vert和face

#include <stdlib.h>//用来产生随机数rand(){rand()%(b-a+1)+a,代表[a,b]随机数}


//定义了一些常见颜色
const TGAColor white = TGAColor(255, 255, 255, 255);
const TGAColor black = TGAColor(0, 0, 0, 255);
const TGAColor red = TGAColor(255, 0, 0, 255);
const TGAColor green = TGAColor(0, 255, 0, 255);
const TGAColor blue = TGAColor(0, 0, 255, 255);

//背景框架的大小
const int frameWidth = 500;
const int frameHeight = 500;

//在外部定义模型，这样可以在各个函数里面直接使用，而不用传参
Model* model = NULL;

Vec3f cameraPos(0, 0, 5);//照相机位置
Vec3f lightDir(0, 0, -1); // 定义了光线方向，也就是向下


//用这样的方式求线段更加准确
void drawLine(Vec2i p0, Vec2i p1, TGAImage& image, TGAColor color) {

    //如果斜率太大，要进行交换，不然点太稀疏
    bool steep = false;
    if (std::abs(p0.x - p1.x) < std::abs(p0.y - p1.y)) {
        std::swap(p0.x, p0.y);
        std::swap(p1.x, p1.y);
        steep = true;
    }

    //保证从左往右画的顺序，不然有负数会出问题
    if (p0.x > p1.x) {
        std::swap(p0, p1);
    }

    //通过公式求出x，y的值
    for (int x = p0.x; x <= p1.x; x++) {
        float t = (x - p0.x) / (float)(p1.x - p0.x);//不加float会出问题
        int y = p0.y * (1. - t) + p1.y * t;
        if (steep) {
            image.set(y, x, color);
        }
        else {
            image.set(x, y, color);
        }
    }
}

//求重心坐标
Vec3f barycentric(Vec3f* pts, Vec3f P) {

    //利用重心坐标先初步得到它的三个重心坐标结果[u,v,1和AB,AC,PA正交，所以将x坐标与y坐标进行叉乘]
    Vec3f u = (Vec3f(pts[2].x - pts[0].x, pts[1].x - pts[0].x, pts[0].x - P.x) ^ Vec3f(pts[2].y - pts[0].y, pts[1].y - pts[0].y, pts[0].y - P.y));
    /* 当u.z值小于1时，几乎可以认为这是一个退化三角形，也就是接近一条直线面积特别小，（至于为什么可以去看u.z是个什么东西，）不过这也是种初步估算 */
    if (std::abs(u.z) < 1e-2) return Vec3f(-1, 1, 1);
    //返回的vec3f就是P的重心坐标（/u.z是因为将范围限定到小于1）
    return Vec3f(1.f - (u.x + u.y) / u.z, u.y / u.z, u.x / u.z);
}



//填充三角形
void fillTriangle(Vec3f* pts,Vec2i* uvS, float * ZBuffer,TGAImage& image,float intensity) {

    //求AABB边界，

    Vec2f bboxmin(image.get_width() - 1, image.get_height() - 1);
    Vec2f bboxmax(0, 0);

    
    //用屏幕矩形添加了边界框的剪切，以节省屏幕外三角形的 CPU 时间]
    bboxmin.x = std::max(.0f, std::min(pts[0].x, std::min(pts[1].x, pts[2].x)));
    bboxmin.y = std::max(.0f, std::min(pts[0].y, std::min(pts[1].y, pts[2].y)));
    bboxmax.x = std::min(image.get_width() - 1.0f, std::max(pts[0].x, std::max(pts[1].x, pts[2].x)));
    bboxmax.y = std::min(image.get_height() - 1.0f, std::max(pts[0].y, std::max(pts[1].y, pts[2].y)));

    //P是AABB包围盒里的一个点，如果在三角形内就光栅化，不在就不画
    Vec3f P;
    for (P.x = bboxmin.x; P.x <= bboxmax.x; P.x++) {
        for (P.y = bboxmin.y; P.y <= bboxmax.y; P.y++) {

            //求出它的重心坐标
            Vec3f bc_screen = barycentric(pts, P);
            

            //如果有重心坐标＜0，说明这个点不在三角形内
            if (bc_screen.x < 0 || bc_screen.y < 0 || bc_screen.z < 0) continue;

            //uv重心坐标
            Vec2i uvP;


            //求出深度z的坐标
            P.z = bc_screen * Vec3f(pts[0].z, pts[1].z, pts[2].z);

            //求出当前这个点的uv重心坐标
            uvP = uvS[0] * bc_screen.x + uvS[1] * bc_screen.y + uvS[2] * bc_screen.z;

            //通过diffuse函数实现uv坐标到uvColor的映射
            TGAColor color = model->diffuse(uvP);
            
            //如果是离的最近的点，就画并且更新ZBuffer，不是就跳过/*这里暂时有个问题，如果没有引入intensity，反而效果更好，这是为什么呢*/

            
            if (P.z > ZBuffer[int(P.x + P.y * frameWidth)]) {
                image.set(P.x, P.y, TGAColor(color[0]*intensity,color[1]*intensity,color[2]*intensity,255));
                //image.set(P.x, P.y, TGAColor(255* intensity, 255 * intensity,255 * intensity, 255));
                ZBuffer[int(P.x + P.y * frameWidth)] = P.z;
            }
           
        }
    }
}

//画出三角形的三条边
void drawTriangle(Vec2i* tri, TGAImage& image, TGAColor color) {
    drawLine(tri[0] ,tri[1], image, color);
    drawLine(tri[1], tri[2], image, color);
    drawLine(tri[2], tri[0], image, color);
}

//看光照强度有多大；
//用点乘的方式来看两个向量的夹角有多大，结果越大说明它们越平行，结果越小说明越垂直，也就是光照效果更好a·b=|a||b|cosθ；{|a|和|b|都是单位向量等于1，那也就是a·b越大，cosθ越大，说明光线与法线向量越平行，也就说明光线与平面越垂直，也就是光线越强}
float getIntensity(Vec3f* world_tri, Vec3f light_dir) {

    Vec3f n = ((world_tri[2] - world_tri[0]) ^ (world_tri[1] - world_tri[0]));//用两条边的叉乘可以得到法线方向

    n.normalize();//进行归一化

    float intensity = n * (light_dir);
    return intensity;
}



//将三维顶点坐标转换到矩阵形式
Matrix vert2Mat(Vec3f v)
{
    Matrix m(4, 1);
    m[0][0] = v.x;
    m[1][0] = v.y;
    m[2][0] = v.z;
    m[3][0] = 1.0f;
    return m;
}

//模型变换矩阵
Matrix modelMatrix() {

    return  Matrix::identity(4);
}

//视图变换矩阵
Matrix viewMatrix() {

    return  Matrix::identity(4);
}

//投影变换矩阵
Matrix projectionMatrix() {
    Matrix resMat(Matrix::identity(4));
    resMat[3][2] = (-1/cameraPos[2]);
    return resMat;
}

//生成NDC坐标(规范化设备坐标)[它是通过将裁剪坐标除以w得出的。它被称为透视除法。它更像窗口（屏幕）坐标，但尚未平移和缩放到屏幕像素。现在，值的范围在所有 3 个轴上都从 -1 到 1 标准化]
Vec3f getNDCCoord(Matrix matTrans, Vec3f v) {

    Matrix matP=vert2Mat( v);
    Matrix resultMat = matTrans * matP;
    Vec3f NDCCoord;
    //透视除法，得到NDC坐标
    for (int i = 0; i < 3; i++) {
        NDCCoord[i] = resultMat[i][0] / resultMat[3][0];
    }
    return NDCCoord;
}

//视口变换，将NDC坐标转换到屏幕坐标

Vec3f viewPortTrans(Vec3f NDC) {


    //Vec3f res = Vec3f(int((resVec.x + 1.) * frameWidth / 2. + .5), int((resVec.y + 1.) * frameWidth / 2. + .5), resVec.z);
    //return Vec3f(int((v.x + 1.) * frameWidth / 2. + .5), int((v.y + 1.) * frameWidth / 2. + .5), v.z);//正交映射[+0.5是因为int是向下取整直接裁掉，额外+0.5可以模拟四舍五入]

    Vec3f res = Vec3f(int((NDC[0] + 1.) * frameWidth / 2. + .5), int((NDC[1] + 1.) * frameWidth / 2. + .5), NDC[2]);
    return res;
}


int main(int argc, char** argv) {

    TGAImage frame(frameWidth,frameHeight,TGAImage::RGB);
    model=new Model("obj/african_head.obj");

    //ZBuffer是记录深度的，本来是二维的，但也可以用这样的方式进行表达
    float ZBuffer[frameWidth * frameHeight];

   //通过MVP得到变换矩阵
    Matrix Trans = projectionMatrix() * viewMatrix() * modelMatrix();


     //根据模型进行绘画
    for (int i = 0; i < model->nfaces(); i++) {
        //model->face(i)，根据索引号找到面，一个面里有三个值，分别是三个顶点的编号 
        std::vector<int>  face(model->face(i));
        
        Vec3f sreen_tri[3];//平面光栅化绘制时的三角形，也就是二维[引入ZBuffer这个概念后，也变成三维了]
        Vec3f world_tri[3];//实际的三维三角形，用来与光线做点积，看光线强度有多大
        
        //遍历三角面板上的三个顶点
        for (int j = 0; j < 3; j++) {
            //面里存的是顶点的编号，依次取出三角形的三个顶点
            Vec3f v(model->vert(face[j]));
           
            //通过投影，透视除法，得到NDC坐标
            Vec3f NDCCoord = getNDCCoord(Trans, v);

            //视口变换，将NDC坐标转换到屏幕坐标
            sreen_tri[j] = viewPortTrans(NDCCoord);
            world_tri[j] = v;
        }

        float intensity = getIntensity(world_tri, lightDir);

        if (intensity > 0) {//光线intensity＜0说明背光看不见，那就不用画了[不然背面那些看不见的三角面板由于是黑色，反而会遮盖了前面的白色三角面板]

            //找到三个顶点的uv坐标
            Vec2i uv[3];
            for(int j=0;j<3;j++){ uv[j]=(model->uv(i, j)); }


            //图片最后呈现黑白色可以理解为[0,0,0]是黑色,[255,255,255]是白色，那如果[c,c,c]rgb三个颜色相等，那也就是黑白中间的颜色，也就是灰色
            fillTriangle(sreen_tri,uv,ZBuffer, frame, intensity);
        }
        //用随机颜色随机填充三角形
        /*int randR = rand() % 256;
        int randG = rand() % 256;
        int randB = rand() % 256;

        fillTriangle(tri, frame, TGAColor(randR,randG,randB,255));*/
    }

    frame.flip_vertically(); // 将图像进行垂直翻转
    frame.write_tga_file("output.tga");
    return 0;
}